// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <assert.h>
#include <errno.h>
#include <lib/zircon-internal/xorshiftrand.h>

#include "util.h"

#define FAIL -1
#define BUSY 0
#define DONE 1

#define FBUFSIZE 65536

static_assert(FBUFSIZE == ((FBUFSIZE / sizeof(uint64_t)) * sizeof(uint64_t)),
              "FBUFSIZE not multiple of uint64_t");

typedef struct worker worker_t;

struct worker {
  worker_t* next;
  int (*work)(worker_t* w);

  rand64_t rdata;
  rand32_t rops;

  int fd;
  int status;
  uint32_t flags;
  uint32_t size;
  uint32_t pos;

  union {
    uint8_t u8[FBUFSIZE];
    uint64_t u64[FBUFSIZE / sizeof(uint64_t)];
  };

  char name[256];
};

static worker_t* all_workers;

bool worker_new(const char* where, const char* fn, int (*work)(worker_t* w), uint32_t size,
                uint32_t flags);
int worker_writer(worker_t* w);
static bool init_environment();

#define F_RAND_IOSIZE 1
#define KB(n) ((n)*1024)
#define MB(n) ((n)*1024 * 1024)

struct {
  int (*work)(worker_t*);
  const char* name;
  uint32_t size;
  uint32_t flags;
} WORK[] = {
    {
        worker_writer,
        "file0000",
        KB(512),
        F_RAND_IOSIZE,
    },
    {
        worker_writer,
        "file0001",
        MB(10),
        F_RAND_IOSIZE,
    },
    {
        worker_writer,
        "file0002",
        KB(512),
        F_RAND_IOSIZE,
    },
    {
        worker_writer,
        "file0003",
        KB(512),
        F_RAND_IOSIZE,
    },
    {
        worker_writer,
        "file0004",
        KB(512),
        0,
    },
    {
        worker_writer,
        "file0005",
        MB(20),
        0,
    },
    {
        worker_writer,
        "file0006",
        KB(512),
        0,
    },
    {
        worker_writer,
        "file0007",
        KB(512),
        0,
    },
};

int worker_rw(worker_t* w, bool do_read) {
  if (w->pos == w->size) {
    return DONE;
  }

  // offset into buffer
  uint32_t off = w->pos % FBUFSIZE;

  // fill our content buffer if it's empty
  if (off == 0) {
    for (unsigned n = 0; n < (FBUFSIZE / sizeof(uint64_t)); n++) {
      w->u64[n] = rand64(&w->rdata);
    }
  }

  // data in buffer available to write
  uint32_t xfer = FBUFSIZE - off;

  // do not exceed our desired size
  if (xfer > (w->size - w->pos)) {
    xfer = w->size - w->pos;
  }

  if ((w->flags & F_RAND_IOSIZE) && (xfer > 3000)) {
    xfer = 3000 + (rand32(&w->rops) % (xfer - 3000));
  }

  ssize_t r;
  if (do_read) {
    uint8_t buffer[FBUFSIZE];
    if ((r = emu_read(w->fd, buffer, xfer)) < 0) {
      fprintf(stderr, "worker('%s') read failed @%u: %d\n", w->name, w->pos, errno);
      return FAIL;
    }

    if (memcmp(buffer, w->u8 + off, r)) {
      fprintf(stderr, "worker('%s) verify failed @%u\n", w->name, w->pos);
      return FAIL;
    }
  } else {
    if ((r = emu_write(w->fd, w->u8 + off, xfer)) < 0) {
      fprintf(stderr, "worker('%s') write failed @%u: %d\n", w->name, w->pos, errno);
      return FAIL;
    }
  }

  // advance
  w->pos += r;
  return BUSY;
}

int worker_verify(worker_t* w) {
  int r = worker_rw(w, true);
  if (r == DONE) {
    emu_close(w->fd);
  }
  return r;
}

int worker_writer(worker_t* w) {
  int r = worker_rw(w, false);
  if (r == DONE) {
    if (emu_lseek(w->fd, 0, SEEK_SET) != 0) {
      fprintf(stderr, "worker('%s') seek failed: %s\n", w->name, strerror(errno));
      return FAIL;
    }
    // start at 0 and reset our data generator seed
    srand64(&w->rdata, w->name);
    w->pos = 0;
    w->work = worker_verify;
    return BUSY;
  }
  return r;
}

bool worker_new(const char* where, const char* fn, int (*work)(worker_t* w), uint32_t size,
                uint32_t flags) {
  worker_t* w = (worker_t*)calloc(1, sizeof(worker_t));
  ASSERT_NE(w, nullptr);

  snprintf(w->name, sizeof(w->name), "%s%s", where, fn);
  srand64(&w->rdata, w->name);
  srand32(&w->rops, w->name);
  w->size = size;
  w->work = work;
  w->flags = flags;

  if ((w->fd = emu_open(w->name, O_RDWR | O_CREAT | O_EXCL, 0644)) < 0) {
    fprintf(stderr, "worker('%s') cannot create file\n", w->name);
    free(w);
    return false;
  }

  if (all_workers) {
    w->next = all_workers;
  }

  all_workers = w;

  return true;
}

int do_work() {
  uint32_t busy_count = 0;
  for (worker_t* w = all_workers; w != nullptr; w = w->next) {
    if (w->status == BUSY) {
      busy_count++;
      if ((w->status = w->work(w)) == FAIL) {
        return FAIL;
      }
      if (w->status == DONE) {
        fprintf(stderr, "worker('%s') finished\n", w->name);
      }
    }
  }
  return busy_count ? BUSY : DONE;
}

bool do_all_work() {
  BEGIN_HELPER;
  for (;;) {
    int r = do_work();
    ASSERT_NE(r, FAIL);
    if (r == DONE) {
      break;
    }
    ASSERT_EQ(run_fsck(), 0);
  }
  END_HELPER;
}

static bool init_environment() {
  all_workers = nullptr;

  // assemble workers
  const char* where = "::";
  for (unsigned n = 0; n < fbl::count_of(WORK); n++) {
    ASSERT_TRUE(worker_new(where, WORK[n].name, WORK[n].work, WORK[n].size, WORK[n].flags));
  }
  return true;
}

bool TestWorkSingleThread(void) {
  BEGIN_TEST;

  ASSERT_TRUE(init_environment());
  ASSERT_TRUE(do_all_work());
  worker_t* w = all_workers;
  worker_t* next;
  while (w != NULL) {
    next = w->next;
    free(w);
    w = next;
  }

  END_TEST;
}

RUN_MINFS_TESTS(rw_workers_test, RUN_TEST_MEDIUM(TestWorkSingleThread))
